/*
 * Copyright (C) 2012 DataSymphony (http://datasymphony.com.au)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package au.com.ds.restty.cmd.impl;

import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import au.com.ds.restty.ExecutionContext;
import au.com.ds.restty.Out;
import au.com.ds.restty.cmd.Command;
import au.com.ds.restty.cmd.Template;
import au.com.ds.restty.exc.InvalidInput;
import au.com.ds.restty.exc.InvalidTemplate;
import au.com.ds.restty.exc.JavaScriptError;
import au.com.ds.restty.fmt.Formatter;
import au.com.ds.restty.fmt.FormatterFactory;
import au.com.ds.restty.fmt.FormatterType;
import au.com.ds.restty.fmt.JsonFormatter;
import au.com.ds.restty.http.HttpConnector;
import au.com.ds.restty.http.RequestMethod;
import au.com.ds.restty.js.JavaScriptEngine;
import au.com.ds.restty.js.JsAnyObject;
import au.com.ds.restty.js.JsArray;
import au.com.ds.restty.js.JsFunction;
import au.com.ds.restty.js.JsObject;
import au.com.ds.restty.js.JsObjectFactory;
import au.com.ds.restty.js.JsObjectFormatter;
import au.com.ds.restty.js.JsObjectType;

public class CommandImpl implements Command {
	private static final Logger LOG = LoggerFactory.getLogger(CommandImpl.class); 
	public static final String FLD_ARGS = "arguments";
	public static final String FLD_URL = "url";
	public static final String FLD_METHOD = "method";
	public static final String FLD_HEADER = "header";
	public static final String FLD_BODY = "body";

	private JavaScriptEngine mJavaScript;
	private Template mPrototypeTemplate;
	private Template mCommandTemplate;

	private JsObject mDef1;
	private JsObject mDef2;

	private String mName;
	private RequestMethod mMethod;
	private String mUrl;
	private Map<String, String> mHeader;
	private String mBody;

	private JsAnyObject mResult;

	protected CommandImpl(Template proto, Template tmpl){
		mPrototypeTemplate = proto;
		mCommandTemplate = tmpl;
		mName = tmpl.getCommandName();
		mDef1 = mPrototypeTemplate.getTemplateObject();
		mDef2 = mCommandTemplate.getTemplateObject();
		mHeader = new HashMap<String, String>();
	}

	@Override
	public JsAnyObject exec(HttpConnector http) {
		prepareAllParameters();

		String result = http.callService(this);

		if (result.isEmpty()) {
			mResult = JsObjectFactory.getJsString("");
			return mResult;
		}

		try {
			mResult = mJavaScript.execute(result, "{json}");
		} catch (Exception e){			
			final String msg = "Error during response processing for command: " + mName + ".\n Raw response: \n" + result;
			Out.normal(msg);
			LOG.error(msg, e);
			throw new JavaScriptError("Error during response processing for command: " + mName, e);			
		}

		return mResult;
	}

	@Override
	public JsAnyObject getResultObject() {
		return mResult;
	}

	@Override
	public String getUrl() {
		return mUrl;
	}

	@Override
	public Map<String, String> getHeader() {
		return mHeader;
	}

	@Override
	public String getBody() {
		return mBody;
	}

	@Override
	public RequestMethod getMethod() {
		return mMethod;
	}

	@Override
	public String getName() {
		return mName;
	}

	private void prepareAllParameters(){
		mJavaScript = JavaScriptEngine.getInstance();

		// prepare arguments
		List<String> undefined = null;
		try {
			LinkedHashMap<String, JsAnyObject> argDefinition = new LinkedHashMap<String, JsAnyObject>();
			prepareArgs(mDef1.get(FLD_ARGS), argDefinition);
			prepareArgs(mDef2.get(FLD_ARGS), argDefinition);		
			ExecutionContext.get().getArguments().addDefinition(argDefinition);
			undefined = ExecutionContext.get().getArguments().getUndefinedNames();			
		} catch (Exception e) {
			throw new InvalidTemplate("Error during preparing argument values for command: " + mName, e);						
		}

		if (!undefined.isEmpty()){
			final String msg = "Undefined arguments: " + undefined + "for command: " + mName;
			Out.normal(msg);
			LOG.error(msg);
			throw new InvalidInput(msg);			
		}

		// prepare url
		// TODO: also add description to templates and help
		try {
			mUrl = JsObjectFormatter
						.optional()
						.expects(JsObjectType.STRING)
						.supportsExpressions(mJavaScript)
						.format(mDef1.get(FLD_URL));
			mUrl += JsObjectFormatter
						.optional()
						.expects(JsObjectType.STRING)
						.supportsExpressions(mJavaScript)
						.format(mDef2.get(FLD_URL));			
		} catch (Exception e) {
			throw new InvalidTemplate("Error during preparing url value for command: " + mName, e);						
		}

		// prepare method
		String method = null;
		try {
			method = JsObjectFormatter
						.optional()
						.expects(JsObjectType.STRING)
						.supportsExpressions(mJavaScript)
						.format(mDef2.get(FLD_METHOD));
			
			if (method.trim().isEmpty()){
				method = JsObjectFormatter
							.optional()
							.expects(JsObjectType.STRING)
							.supportsExpressions(mJavaScript)
							.format(mDef1.get(FLD_METHOD));
			}
		} catch (Exception e) {
			throw new InvalidTemplate("Error during preparing method value for command: " + mName, e);						
		}

		try {
			mMethod = RequestMethod.valueOf(method);
		} catch (Exception e){
			throw new InvalidTemplate("Invalid HTTP method defined in template: " + method + "for command: " + mName, e);			
		}

		// prepare headers
		try {
			JsObject header1 = mDef1.get(FLD_HEADER).as(JsObject.class);
			JsObject header2 = mDef2.get(FLD_HEADER).as(JsObject.class);
			if (header2 != null) {
				for (Map.Entry<JsAnyObject, JsAnyObject> e : header2.getValue().entrySet()) {
					String name = JsObjectFormatter
										.required()
										.expects(JsObjectType.STRING)
										.format(e.getKey());
					String value = JsObjectFormatter
										.required()
										.expects(JsObjectType.STRING)
										.supportsExpressions(mJavaScript)
										.format(e.getValue());
					mHeader.put(name, value);
				}		
			}

			if (header1 != null) {
				for (Map.Entry<JsAnyObject, JsAnyObject> e : header1.getValue().entrySet()) {
					String name = JsObjectFormatter
										.required()
										.expects(JsObjectType.STRING)
										.format(e.getKey());
					if (!mHeader.containsKey(name)) {
						String value = JsObjectFormatter
											.required()
											.expects(JsObjectType.STRING)
											.supportsExpressions(mJavaScript)
											.format(e.getValue());
						mHeader.put(name, value);			
					}
				}		
			}
		} catch (Exception e){
			throw new InvalidTemplate("Error during preparing headers for command: " + mName, e);			
		}

		// prepare body
		try {
			prepareBody(mDef2.get(FLD_BODY));		
			if (mBody.trim().isEmpty()){
				prepareBody(mDef1.get(FLD_BODY));
			}
		} catch (Exception e) {
			throw new InvalidTemplate("Error during preparing request body for command: " + mName, e);						
		}
	}

	private void prepareArgs(JsAnyObject args, LinkedHashMap<String, JsAnyObject> result){
		JsArray a = args.as(JsArray.class);
		if (a != null){
			for (JsAnyObject o : a.getValue()){
				result.put(JsObjectFormatter
								.required()
								.expects(JsObjectType.STRING)
								.format(o), 
							JsObjectFactory.getJsUndefined());
			}
		} else {
			JsObject o = args.as(JsObject.class);
			if (o != null) {
				for (Map.Entry<JsAnyObject, JsAnyObject> e : o.getValue().entrySet()){
					result.put(JsObjectFormatter
									.required()
									.expects(JsObjectType.STRING)
									.format(e.getKey()), 
								e.getValue());
				}
			} else {
				throw new InvalidTemplate("Invalid argument type: " + args.getType() + "; expected: [ARRAY, OBJECT]");
			}
		}
	}

	private void prepareBody(JsAnyObject body){
		if (body != null && body.getType() == JsObjectType.FUNCTION){
			body = mJavaScript.call((JsFunction) body);
		}

		if (body != null && (body.getType() == JsObjectType.OBJECT || body.getType() == JsObjectType.ARRAY)){
			Formatter fmt = FormatterFactory.getFormatter(FormatterType.JSON);
			HashMap<String, Object> options = new HashMap<String, Object>(); options.put(JsonFormatter.OPT_STRIP_QUOTES, new Boolean(false));
			mBody = fmt.format(body, options);
		} else {
			mBody = JsObjectFormatter
						.optional()
						.expects(JsObjectType.STRING)
						.supportsExpressions(mJavaScript)
						.format(body);
		}
	}
}
